package cn.aiweiyi.qingjing.coupon.distribution.service.impl;

import cn.aiweiyi.qingjing.coupon.comm.constant.Constant;
import cn.aiweiyi.qingjing.coupon.comm.constant.CouponStatus;
import cn.aiweiyi.qingjing.coupon.comm.exception.CouponException;
import cn.aiweiyi.qingjing.coupon.distribution.entity.Coupon;
import cn.aiweiyi.qingjing.coupon.distribution.service.RedisService;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author 青衿
 * @Date 2020/11/19 10:26
 * 分发业务相关的Redis缓存操作
 **/
@Slf4j
@Service
public class RedisServiceImpl implements RedisService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    private String status2RedisKey(Integer status, Long userId) {
        String redisKey = null;
        CouponStatus couponStatus = CouponStatus.of(status);
        switch (couponStatus) {
            case USABLE:
                redisKey = String.format("%s%s",
                        Constant.RedisPrefix.USER_COUPON_USABLE, userId);
                break;
            case USED:
                redisKey = String.format("%s%s",
                        Constant.RedisPrefix.USER_COUPON_USED, userId);
                break;
            case EXPIRED:
                redisKey = String.format("%s%s",
                        Constant.RedisPrefix.USER_COUPON_EXPIRED, userId);
                break;
        }
        return redisKey;
    }

    /**
     * 根据userId和状态找到缓存的优惠券列表数据
     * 查询缓存中的优惠券数据
     *
     * @param userId 用户id
     * @param status 优惠券状态 {@link CouponStatus}
     * @return {@link Coupon}s,注意,可能会返回null,代表从没有过记录
     */
    @Override
    public List<Coupon> getCachedCoupons(Long userId, Integer status) {
        log.info("从缓存获取优惠券信息,用户:{},状态:{}", userId, status);
        String redisKey = status2RedisKey(status, userId);//构建key
        //根据key取出该用户该状态的所有优惠券信息,得到优惠券信息JSON字符串的集合
        List<String> couponStr = stringRedisTemplate.opsForHash().values(redisKey)
                .stream()
                .map(o -> Objects.toString(o))//map把o转换为toString(o)
                .collect(Collectors.toList());//collect把转换后的收集为集合
        //如果为空,向Redis存入一个空优惠券集合,防止缓存穿透
        if (CollectionUtils.isEmpty(couponStr)) {
            saveEmptyCouponListToCache(userId, Collections.singletonList
                    (status));
            return Collections.emptyList();//返回空集合
        }
        //如果不为空,把取出的优惠券JSON一个个还原成Java对象,用集合装起来
        return couponStr.stream()
                .map(cs -> JSON.parseObject(cs, Coupon.class))
                .collect(Collectors.toList());
    }

    /**
     * 保存空的优惠券列表到缓存中,为了避免缓存穿透
     * 避免优惠券查询缓存穿透
     *
     * @param userId 用户id
     * @param status 优惠券状态列表
     * @return {@link Coupon}s,注意,可能返回null,表示没有过记录
     */
    @Override
    public void saveEmptyCouponListToCache(Long userId, List<Integer> status) {
//        log.info("为用户{}保存空的优惠券集合到Reids,状态{}",
//                userId, JSON.toJSONString(status));
//        //正常来讲,这里的key是coupon_id,value是序列化的Coupon
//        //此处用-1作为coupon_id,用吴晓东Coupon对象作为value
//        Map<String, String> invalidCouponMap = new HashMap<>();
//        invalidCouponMap.put("-1", JSON.toJSONString(Coupon.invalidCoupon));
//        //使用SessionCallback把数据命令放入到Redis的pipeline
//        SessionCallback<Object> callback = new SessionCallback<Object>() {
//            @Override
//            public Object execute(RedisOperations op) throws DataAccessException {
//                status.forEach(s -> {
//                    String redisKey = status2RedisKey(s, userId);
//                    op.opsForHash().putAll(redisKey, invalidCouponMap);
//                    //无效数据,所有保留在缓存的时间不要太长
////                    redisTemplate.expire(redisKey,60, TimeUnit.SECONDS);
//                });
//                return null;
//            }
//        };
//        log.info("Redis管道执行结果{}", JSON.toJSONString(
//                stringRedisTemplate.executePipelined(callback)
//        ));
    }

    /**
     * 领取优惠券码
     * 尝试从 Cache中获取一个优惠券码
     *
     * @param templateId 优惠券模板主键
     * @return 优惠券码
     */
    @Override
    public String tryToAcquireCouponCodeFromCache(Integer templateId) {
        //构建模板的Redis Key
        String templateKey = String.format("%s%s",
                Constant.RedisPrefix.COUPON_TEMPLATE, templateId);
        //根据模板Key按List类型随便取出(pop)一个优惠券码  ketai_coupon_template_code_20
        String couponCode = stringRedisTemplate.opsForList().leftPop(templateKey);
        log.info("根据模板{}和key{}取出一个优惠券码:{}",
                templateId, templateKey, couponCode);
        return couponCode;
    }

    /**
     * 添加优惠券到缓存中
     * 讲优惠券保存到Cache中
     *
     * @param userId  用户id
     * @param coupons {@link Coupon}s
     * @param status  优惠券状态
     * @return 保存成功的个数
     * @throws CouponException
     */
    @Override
    public Integer addCouponToCache(Long userId, List<Coupon> coupons, Integer status) throws CouponException {
        log.info("为用户{}添加{}状态的优惠券:{}",
                userId, status, JSON.toJSONString(coupons));
        Integer result = -1;
        CouponStatus couponStatus = CouponStatus.of(status);
        switch (couponStatus) {
            case USABLE:
                result = addCouponToCache4Usable(userId, coupons);
                break;

            case USED:
                result = addCouponToCache4Usable(userId, coupons);
                break;

            case EXPIRED:
                result = addCouponToCache4Usable(userId, coupons);
                break;
        }
        return result;
    }

    /**
     * 获取一个随机的过期时间,为了防止缓存雪崩: key在同一时间失效
     *
     * @param min 最小的小时数
     * @param max 最大的小时数
     * @return 返回{min,max}之间的随机秒数
     */
    private Long getRandomExpirationTime(Integer min, Integer max) {
        //来自org.apache.commons.lang3包的随机工具类
        return RandomUtils.nextLong(
                min * 60 * 60,
                max * 60 * 60
        );
    }

    /**
     * 为用户添加可使用(USABLE)状态的优惠券信息
     *
     * @param userId  用户id
     * @param coupons 优惠券集合
     * @return 缓存成功的优惠券数量
     */
    private Integer addCouponToCache4Usable(Long userId, List<Coupon> coupons) {
        log.info("添加Usable状态的优惠券到缓存");
        Map<String, String> needCacheCoupons = new HashMap<>();
        coupons.forEach(c ->
                needCacheCoupons.put(
                        c.getId().toString(),
                        JSON.toJSONString(c)
                ));

        String redisKey = status2RedisKey(CouponStatus.USABLE.getCode(),
                userId);//生成用户可用状态的key
        stringRedisTemplate.opsForHash().putAll(redisKey, needCacheCoupons);//保存
        log.info("为用户{}添加{}个优惠券到缓存,key:{}",
                userId, needCacheCoupons.size(), redisKey);
        stringRedisTemplate.expire(redisKey, getRandomExpirationTime(1, 2),
                TimeUnit.SECONDS);//过期时间1-2小时之间的一个随机秒数
        return needCacheCoupons.size();
    }

    /**
     * 为用户把已使用(USED)的优惠券信息存入Redis缓存
     *
     * @param userId  用户id
     * @param coupons 已使用优惠券集合
     * @return 存入缓存的数量
     * @throws CouponException
     */
    private Integer addCouponToCache4Used(Long userId, List<Coupon> coupons) throws CouponException {
        //执行本业务操作,会影响USABLE和USED两个缓存的数据,因为加入已使用的缓存,
        //说明之前这些优惠券肯定是USABLE状态的,因此需要去掉USABLE中的相关的数据,然后
        //改为添加到USED中,因此,需要生成两个key,来进行对比校验
        log.info("为用户{}添加已使用状态的优惠券", userId);
        Map<String, String> needCacheUsed = new HashMap<>();
        String usableKey = status2RedisKey(CouponStatus.USABLE.getCode()
                , userId);

        String usedKey = status2RedisKey(CouponStatus.USED.getCode(),
                userId);
        //用户当前用户可用的优惠券
        List<Coupon> usableCoupons = getCachedCoupons(userId,
                CouponStatus.USABLE.getCode());
        //查询Redis中优惠券如果没抓到,会存入一个无效优惠券,因此可用的数量一定是不会比
        //用户传进来的数量少的,以断言的方式确保这一点
        assert usableCoupons.size() >= coupons.size();
        coupons.forEach(c -> needCacheUsed.put(
                c.getId().toString(), JSON.toJSONString(c)
        ));

        //校验当前要操作的优惠券是否与缓存中的数据匹配
        List<Integer> usableCouponIds = usableCoupons.stream().map
                (Coupon::getId).collect(Collectors.toList());

        List<Integer> paramCouponIds = coupons.stream().map
                (Coupon::getId).collect(Collectors.toList());

        //如果传入的优惠券id不是缓存中可以优惠券id的子集,报错
        if (!CollectionUtils.isSubCollection(usableCouponIds,
                paramCouponIds)) {
            log.error("要存入的已使用的优惠券数据与缓存中不匹配," +
                            "要存入的:{},缓存中的:{},用户:{}",
                    JSON.toJSONString(coupons),
                    JSON.toJSONString(usableCoupons), userId);
            throw new CouponException("要存入的已使用优惠券数据与缓存中不匹配");
        }
        //准备清理原缓存的USABLE相关优惠券数据,构建要清理的key集合
        List<String> needCleankey = paramCouponIds.stream()
                .map(i -> i.toString()).collect(Collectors.toList());
        //用Spring Data Redis的SessionCallback向Redis发送一批操作命令,管道式执行
        SessionCallback<Objects> sessionCallback = new SessionCallback<Objects>() {

            //匿名内部类实现execute方法
            @Override
            public Objects execute(RedisOperations operations) throws DataAccessException {
                //1.已使用的优惠券Cache缓存添加
                operations.opsForHash().putAll(
                        usedKey, needCacheUsed);

                //2.可用的优惠券 Cache需要清理
                operations.opsForHash().delete(
                        usableKey, needCleankey.toArray());
                //3.重置过期时间
                operations.expire(
                        usableKey,
                        getRandomExpirationTime(1, 2), TimeUnit.SECONDS);

                //4.设置刚添加的已使用优惠券的过期时间
                operations.expire(
                        usedKey,
                        getRandomExpirationTime(1, 2), TimeUnit.SECONDS);
                return null;
            }
        };
        log.info("Redis命令管道执行结果:{}", JSON.toJSONString(
                stringRedisTemplate.executePipelined(sessionCallback)));
        return coupons.size();
    }


    /**
     * 将过期优惠券加入到 Cache中
     *
     * @param userId  用户id
     * @param coupons 过期优惠券
     * @return 更新的数量
     * @throws CouponException 自定义异常
     */
    private Integer addCouponToCache4Expired(Long userId, List<Coupon>
            coupons) throws CouponException {
        //执行本业务操作,会影响到USABLE和EXPIRED两个缓存的数据,因为加入已过期的缓存,
        //说明之前这些优惠券肯定是USABLE状态的,因此需要去掉USABLE中相关的数据,然后
        //改为天啊及到EXPIRED中,因此需要,生成两个key,来进行校验
        log.info("为用户{}添加已过期状态的优惠券", userId);
        Map<String, String> needCacheExpired = new HashMap<>();
        String usableKey = status2RedisKey(
                CouponStatus.USABLE.getCode(), userId);

        String expiredKey = status2RedisKey(
                CouponStatus.EXPIRED.getCode(), userId);

        //用户当前用户可用的优惠券
        List<Coupon> usableCoupons = getCachedCoupons(userId,
                CouponStatus.USABLE.getCode());
        //确保传入的要过期的数量,小于缓存中可用优惠券的数量
        assert usableCoupons.size() >= coupons.size();
        coupons.forEach(c -> needCacheExpired.put(c.getId().toString(),
                JSON.toJSONString(c)));

        //校验要过期的优惠券参数是否与缓存中可用优惠券匹配
        List<Integer> usableIds = usableCoupons.stream()
                .map(Coupon::getId).collect(Collectors.toList());
        List<Integer> paramIds = coupons.stream()
                .map(Coupon::getId).collect(Collectors.toList());

        //如果传入的优惠券id不是缓存中可以优惠券id的子集,报错
        if (!CollectionUtils.isSubCollection(paramIds, usableIds)) {
            log.error("要过期的优惠券和缓存中数据不匹配:{},{},{}",
                    userId, JSON.toJSONString(usableIds),
                    JSON.toJSONString(paramIds));
            throw new CouponException("要过期的优惠券和缓存中的数据不匹配");
        }
        List<String> needCleanKey = paramIds.stream()
                .map(i -> i.toString()).collect(Collectors.toList());
        //用Spring Data Redis的SessionCallback向Redis发送一批操作命令,管道式执行
        SessionCallback<Objects> sessionCallback =
                new SessionCallback<Objects>() {
                    @Override
                    public Objects execute(RedisOperations operations) throws DataAccessException {
                        //1.添加已过期的优惠券缓存
                        operations.opsForHash().putAll(
                                expiredKey, needCacheExpired);

                        //2.可用的优惠券存需要清理
                        operations.opsForHash().delete(
                                usableKey, needCleanKey.toArray());

                        //3.重置可用优惠券的过期时间
                        operations.expire(
                                usableKey,
                                getRandomExpirationTime(1, 2), TimeUnit.SECONDS);

                        //4.设置过期优惠券数据的过期时间
                        operations.expire(
                                expiredKey,
                                getRandomExpirationTime(1, 2), TimeUnit.SECONDS);
                        return null;
                    }
                };
        log.info("Redis命令管道执行结果{}",
                JSON.toJSONString(
                        stringRedisTemplate.executePipelined(sessionCallback)));
        return coupons.size();
    }
}
